발행일

Django 폼 레이아웃을 도와주는 방법들 — 조사, 그리고 실제로 고른 조합

Django 폼 레이아웃을 도와주는 방법들

Django의 폼(formset 포함)은 데이터 바인딩과 검증까지는 해주지만, 레이아웃과 스타일은 결국 별도의 방법이 필요합니다. 그 방법들을 조사했던 글이고, 뒤에 실제로 고른 조합을 붙였습니다.

날짜 정정 — 이 글은 원래 2022-08-05로 게시돼 있었는데, 본문이 Django 5.2 문서(2025-04 출시)와 django-formify(2024 공개)를 참조하므로 성립할 수 없는 날짜입니다. 같은 시기의 다른 사전 조사 글들과 함께 2025-04로 교정합니다.

공식 문서: ModelForm · django-crispy-forms

1. 후보 비교

라이브러리구조 제어스타일 연동주요 특징
django-crispy-formsBootstrap / TailwindPython 코드로 레이아웃 제어 (Layout, Row, Column)
django-formifyTailwind 전용컴포넌트 기반 구조
django-widget-tweaks자유템플릿에서 필드 속성·클래스 조작
django-bootstrap5Bootstrap 전용자동 렌더링, 커스터마이징 제한
django-dynamic-formset자유클라이언트(JS)에서 폼 행 추가/삭제

2. 라이브러리 없이 하는 기본기

어느 쪽을 고르든 밑바탕은 같습니다 — 위젯 attrsMeta 옵션.

# 위젯 커스터마이징 — 클래스·placeholder·HTML 제약을 서버에서 지정
forms.CharField(
    widget=forms.TextInput(attrs={
        "class": "input input-bordered",
        "placeholder": "이름을 입력하세요",
        "maxlength": "30",
    })
)
# ModelForm — 모델 기반 자동 생성 + 필드별 위젯 오버라이드
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content']
        widgets = {
            'content': forms.Textarea(attrs={"class": "w-full p-3 border rounded-md"}),
        }
# 조건부 필드 — __init__에서 사용자·상태에 따라 필드를 조작
def __init__(self, *args, **kwargs):
    user = kwargs.pop('user', None)
    super().__init__(*args, **kwargs)
    if not user.is_staff:
        self.fields.pop('phone')
    self.fields['username'].disabled = True

forms.Field 공통 속성(label, help_text, error_messages, initial, validators 등)과 Meta 속성(fields/exclude/widgets/labels 등)은 문서 표가 잘 정리돼 있어 생략합니다. Form vs ModelForm의 기준은 단순합니다 — 저장할 모델이 있으면 ModelForm, 검색·문의처럼 모델 없는 입력이면 Form.

3. crispy-forms의 Layout

crispy의 핵심은 레이아웃이 템플릿이 아니라 Python 코드에 있다는 것입니다.

self.helper.layout = Layout(
    HTML("<h3 class='text-lg font-bold mb-4'>기본 정보</h3>"),
    Row(Column('username'), Column('email')),
    Row(Column('birth_date'), Column('phone')),
    Submit('submit', '저장하기', css_class='btn btn-primary w-full'),
)

폼 구조가 바뀔 때 템플릿을 안 건드려도 되고, 조건부 레이아웃(__init__에서 layout 조립)이 자연스럽습니다.


4. 실제로 고른 조합 — 하나가 아니라 둘이었다

조사할 때는 표의 다섯 개가 경쟁 관계인 줄 알았습니다. 실제 프로젝트(표준데이터 관리 시스템)의 requirements를 보면 결말이 이렇습니다:

django-crispy-forms==2.3
crispy-bootstrap5==2024.10
django-widget-tweaks==1.5.0

crispy와 widget-tweaks를 같이 씁니다. 역할이 달라서예요 — 구조가 정형화된 폼(등록·수정 화면)은 crispy의 Layout으로 Python에서 조립하고, 템플릿에서 그때그때 클래스 하나 얹거나 속성 하나 바꾸는 미세 조정은 widget-tweaks({{ form.field | add_class:"..." }})로 합니다. 표의 "구조 제어 ✅ / ❌" 축은 우열이 아니라 서로 다른 층이었던 거죠. 비교표를 만들 때 "무엇을 고를까"만 묻고 "같이 쓸 수 있는가"를 안 물었던 게 이 조사의 빈틈이었습니다.

dynamic-formset은 탈락했습니다 — 첨부파일처럼 행을 추가/삭제하는 formset UI는 필요했지만, 라이브러리 대신 management_formTOTAL_FORMS를 직접 조작하는 jQuery 클론 패턴으로 갔어요. 의존성 하나 값을 못 하는 분량이라서요. 그 패턴은 1:1 Q&A 게시판 글의 첨부 절에, 서버 쪽 inline formset 구성은 별도 글에 있습니다.

정리

  • 폼 레이아웃 도구는 "코드에서 구조를 짤 것인가(crispy)"와 "템플릿에서 조정할 것인가(widget-tweaks)"의 두 층이고, 실무 답은 층별로 하나씩이었습니다.
  • 비교표에는 "함께 쓸 수 있는가" 열이 필요했습니다. 경쟁 구도로만 보면 보완 관계를 놓칩니다.